<?php
namespace zake\foundation;

/**
 * !!! 已把功能移入 Process 中，此类只做说明使用
 * !!! 已把功能移入 Process 中，此类只做说明使用
 *-----------------------------------------------------------------------------
 * 【相关概念】
 *
 * 1.进程组
 *      一坨相关的进程可以组成一个进程组，每个进程组都会有一个组ID（正整数），
 *      每个进程组都会有一个组长进程，组长进程的ID等于进程组ID。
 *      组长进程可以创建新的进程组以及该进程组中的其他进程。一个进程组的是有生命周期的，
 *      即便是组长进程挂了，只有组里还有其他的活口，那就就算该进程组依然存活，
 *      只有到组里最后一个活口也挂了，那真的就是彻底没了。
 * 2.会话
 *      一坨相关的进程组组成了一个会话。在*NIX下，是通过setsid()创建一个新的会话。
 *      但是值得注意的是，组长进程不能创建会话，简单理解就是在组长进程中，
 *      执行setsid函数会报错，这点很重要。所以一般都是组长进程执行fork，然后主进程退出，
 *      因为子进程的进程ID是新分配的，而子进程的进程组ID是继承父进程的，
 *      所以子进程就注定不可能是组长进程，从而可以确保子进程中一定可以执行setsid函数。
 *      在执行setsid函数时候，一般会发生下面三个比较重要的事情：
 *          1:该进程会创建一个新的进程组，该进程为进程组组长（或者你可以认为这是一种提升）
 *          2:该进程会创建一个会话组并成为该会话的会话首进程（会话首进程就是创建该会话的进程）
 *          3:该进程会失去控制终端。如果该进程本来就没有控制终端，则罢了（liao）。如果有，那么该进程也将脱离该控制终端，与之失去联系。
 * 3.控制终端
 *      每个会话可能会拥有一个控制终端（看着比较玄学，你可以暂时理解为就一个那种黑乎乎的命令行窗口），
 *      建立与控制终端连接的会话首进程叫做控制进程。
 *
 * --------------------------------------------------------------------------------------------------
 *
 * 【守护进程 创建流程】
 *
 * 1.创建子进程，终止父进程
 *      由于守护进程是脱离控制终端的，因此首先创建子进程，终止父进程，
 *      使得程序在shell终端里造成一个已经运行完毕的假象。
 *      之后所有的工作都在子进程中完成，而用户在shell终端里则可以执行其他的命令，
 *      从而使得程序以僵尸进程形式运行，在形式I上做到了与控制终端的脱离。
 *
 * 2.在子进程中创建新会话
 *      这个步骤是创建守护进程中最重要的一步，在这里使用的是系统函数setsid。
 *      setsid函数用于创建一个新的会话，并担任该会话组的组长。
 *      调用setsid仃三个作用：
 *          1：让进程摆脱原会话的控制、让进程摆脱原进程组的控制和让进程摆脱原控制终端的控制。
 *          2：在调用fork函数时，子进程全盘拷贝父进程的会话期(session，是一个或多个进程组的集合)、进程组、控制终端等，
 *              虽然父进程退出了，但原先的会话期、进程组、控制终端等并没有改变，因此，那还不是真正意义上使两者独立开来。
 *          3：setsid函数能够使进程完全独立出来，从而脱离所有其他进程的控制。
 *
 * 3.改变工作目录
 *      使用fork创建的子进程也继承了父进程的当前工作目录。
 *      由于在进程运行过程中，当前目录所在的文件系统不能卸载，
 *      因此，把当前工作目录换成其他的路径，如“/”或“/tmp”等。
 *      改变工作目录的常见函数是chdir。
 *
 * 4.重设文件创建掩码
 *      文件创建掩码是指屏蔽掉文件创建时的对应位。
 *      由于使用fork函数新建的子进程继承了父进程的文件创建掩码，这就给该子进程使用文件带来了诸多的麻烦。
 *      因此，把文件创建掩码设置为0，可以大大增强该守护进程的灵活性。设置文件创建掩码的函数是umask，
 *      通常的使用方法为umask(0)。
 *
 * 5.关闭文件描述符
 *      用fork新建的子进程会从父进程那里继承一些已经打开了的文件。
 *      这些被打开的文件可能永远不会被守护进程读或写，但它们一样消耗系统资源，
 *      可能导致所在的文件系统无法卸载。
 *
 * ---------------------------------------------------------------------------------
 *
 * 查看所有运行的php进程(可关闭终端后查看)
 * ps aux | grep php
 * wyy               9037   0.0  0.0  4268040    788 s000  S+    9:51AM   0:00.00 grep php
 * wyy               8873   0.0  0.0  4353508   1264   ??  S     9:50AM   0:00.02 php start.php
 *
 * 查看当前终端运行的php程序：如果是守护进程，此处无法查看到php start.php的进程
 * ps au | grep php
 * wyy               9037   0.0  0.0  4268040    788 s000  S+    9:51AM   0:00.00 grep php
 * wyy               8873   0.0  0.0  4353508   1264   ??  S     9:50AM   0:00.02 php start.php
 *
 * ---------------------------------------------------------------------------------
 *
 * 【守护进程】与【后台执行】的区别
 *
 * 后台执行
 * php start.php &
 * 程序进入后台执行，不与终端交互，但如果关闭终端，此时的程序会终止
 *
 * 守护进程
 * php start.php
 * 需要程序支持守护进程，程序执行与终端无关，关闭终端，程序继续执行
 *
 * ---------------------------------------------------------------------------------
 */
class Daemon
{


    /**
     * @var Daemon | null
     */
    private static $singleton = null;

    /**
     * 构造器
     * @return Daemon
     * @author liu.bin 2020-04-03 23:16
     */
    public static function singleton()
    {
        if(is_null(static::$singleton)){
            static::$singleton = new static();
        }
        return static::$singleton;
    }


    private function __construct(){}


    /**
     * 进入守护进程
     *
     * @author liu.bin 2020-04-14 15:41
     */
    public function daemonize($flag=true)
    {
        if(empty($flag)){
            return false;
        }

        // 1：创建子进程
        $pid = pcntl_fork();
        if($pid > 0){
            // 主进程
            echo "master exit.\r\n";
            exit;
        }elseif ($pid < 0){
            // 失败
            echo "child process create fail.\r\n";
            exit;
        }

        // 2：子进程创建新会话
        if(!posix_setsid() ){
            echo "child process setsid error.\r\n";
            exit;
        }

        // 创建子进程
        $pid = pcntl_fork();
        if( $pid  < 0 ){
            // 失败
            echo "child process create fail.\r\n";
            exit;
        } else if( $pid > 0 ) {
            // 主进程
            echo "master exit.\r\n";
            exit;
        }

        // 改变工作目录，守护进程一般不参与终端的读写，可以关闭
        umask(0);
        chdir( '/tmp' );
        fclose(STDIN);
        fclose(STDOUT);
        fclose(STDERR);
        return true;
    }

}
